<?php

namespace App\Exports;

use App\Mail\BaseMail;
use App\Models\AsyncExportTask;
use App\Models\Attachment;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithCustomChunkSize;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Exceptions\NoFilenameGivenException;
use Maatwebsite\Excel\Exceptions\NoFilePathGivenException;
use Maatwebsite\Excel\Exceptions\NoTypeDetectedException;
use Maatwebsite\Excel\Files\Filesystem;
use Maatwebsite\Excel\Helpers\FileTypeDetector;
use Maatwebsite\Excel\Writer;
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
use PhpOffice\PhpSpreadsheet\Exception;

abstract class BaseExport extends DefaultValueBinder implements WithHeadings, WithMapping, ShouldQueue, WithCustomChunkSize
{
    use InteractsWithQueue, Queueable, Exportable, SerializesModels;

    protected $request;

    protected $filePath;

    protected $fileName;

    protected $needSendMail = false;

    /**
     * 任务 id ( channel 为 admin 时, 此时的 id 就是导出表记录里的 id
     * @var
     */
    protected $taskId;

    /**
     * 渠道来源: admin / h5
     * @var
     */
    protected $channel;

    public $timeout = 1800;

    public function __construct(\Illuminate\Http\Request $request = null)
    {
        $this->request = $request !== null ? $request->all() : [];

        !app()->environment('production') && app('config')->set('debugbar.collectors.db', false); // 不收集 sql
    }

    /**
     * 去除 emoji
     * @param $val
     * @return mixed|string
     */
    public function removeEmoji($val)
    {
        if ($val === '') {
            return '';
        }
        $val = json_encode([$val]);
        $val = preg_replace('#(\\\ud[0-9a-f]{3})#i', '', $val);
        return json_decode($val, true)[0] ?? '';
    }

    /**
     * 重写 download 方法, 增加异步导出
     * @param string|null $fileName
     * @param string|null $writerType
     * @param array|null $headers
     * @return \Illuminate\Http\Response|\Laravel\Lumen\Http\ResponseFactory|object|\Symfony\Component\HttpFoundation\BinaryFileResponse
     * @throws Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    public function download(string $fileName = null, string $writerType = null, array $headers = null)
    {
        $headers = $headers ?? $this->headers ?? [];
        $fileName = $this->fileName = $fileName ?? $this->getFileName() ?? null;
        $writerType = $writerType ?? $this->writerType ?? null;

        if (null === $fileName) {
            throw new NoFilenameGivenException();
        }

        // 异步导出
        if (app('request')->has('async')) {
            if (Str::contains(app('request')->path(), 'admin')) {
                $this->adminAsync($fileName);
            } elseif (Str::contains(app('request')->path(), 'h5')) {
                $this->h5Async($fileName);
            } else {
                abort(400, '导出渠道未找到');
            }

            $this->queue();
            return response('', 201);
        }

        return $this->getExporter()->download($this, $fileName, $writerType, $headers);
    }

    /**
     * 重写 queue 方法
     * @param null $queue
     * @param null $command
     */
    public function queue($queue = null, $command = null)
    {
        // queue 在 Queueable 和 Exportable 里都有, 这里冲突了, 特殊处理
        if ($queue === null) {
            if (empty($this->filePath)) {
                throw NoFilePathGivenException::export();
            }
            if ($this instanceof WithMultipleSheets) {
                throw new \RuntimeException('暂不支持多 sheets 异步导出');
            }
            if ($this instanceof FromQuery && ($count = $this->query()->count()) > 100000) {
                abort(400, '请控制数据量在十万条以内,当前数据量: ' . $count);
            }

            dispatch($this);
        } else {
            $queue->push($command, null, $this->queue);
        }
    }

    /**
     * 异步队列 handle
     * @param Writer $writer
     * @param Filesystem $filesystem
     */
    public function handle(Writer $writer, Filesystem $filesystem)
    {
        try {
            if ($this->getChannel() === 'admin') { // 后台导出, 更新状态
                $task = AsyncExportTask::query()->findOrFail($this->getTaskId());
                $task->update(['status' => AsyncExportTask::STATUS_PROCESSING, 'start_at' => Carbon::now()]);
            }

            $temporaryFile = $writer->export($this, FileTypeDetector::detectStrict($this->getFilePath()));
            $filesystem
                ->disk(config('filesystems.default'))
                ->copy(
                    $temporaryFile,
                    $this->getFilePath()
                );
            $temporaryFile->delete();

            if ($this->getChannel() === 'admin') {
                $task->update(['status' => AsyncExportTask::STATUS_SUCCESS, 'end_at' => Carbon::now()]);
            }

            $this->dispatchNextJobInChain(); // next chain 后面的 job
        } catch (\Exception $e) {
            if ($this->getChannel() === 'admin') {
                !is_null($task) && $task->update(['status' => AsyncExportTask::STATUS_FAILED, 'end_at' => Carbon::now(), 'filed_message' => $e->getMessage()]);
            }

            if ($e instanceof NoTypeDetectedException) {
                \Log::error('导出错误(格式不支持):' . $e->getMessage());
            }
            \Log::error('导出发生错误:' . $e->getMessage());
        }
    }

    public function dispatchNextJobInChain()
    {
        if (! empty($this->chained)) {
            $job = tap(unserialize(array_shift($this->chained)), function ($next) {
                $next->chained = $this->chained;

                $next->onConnection($next->connection ?: $this->chainConnection);
                $next->onQueue($next->queue ?: $this->chainQueue);

                $next->chainConnection = $this->chainConnection;
                $next->chainQueue = $this->chainQueue;
            });

            if ($job instanceof BaseMail) { // 邮件特殊处理
                if ($this->isNeedSendMail()) {
                    $job->attachFromStorageDisk('oss', $this->getFilePath(), $this->getFileName()); // TODO 优化 抽象
                }

                $job->queue();
            } else {
                dispatch($job);
            }
        }
    }

    /**
     * 后台异步导出记录
     * @param $fileName
     * @throws NoTypeDetectedException
     * @throws \Throwable
     */
    public function adminAsync($fileName)
    {
        $operatorId = auth()->user()->getAuthIdentifier();
        $eigenvalues = md5(implode(';', [$operatorId, get_class($this), http_build_query($this->request)]));

        /** @var AsyncExportTask $task */
        try {
            \DB::beginTransaction();

            $task = AsyncExportTask::query()->create([
                'task_numbering' => sprintf('%d-%s', $operatorId, Str::uuid()),
                'eigenvalues' => $eigenvalues,
                'task_name' => $fileName,
                'operator_id' => $operatorId,
            ]);
            $this->setFilePath('admin_exports/', $fileName)->setTaskId($task['id'])->setChannel('admin');
            $task->attachments()->sync(Attachment::query()->create(['file' => $this->getFilePath(), 'mime_type' => FileTypeDetector::detectStrict($fileName)]));

            if (null === $this->queue) {
                $this->onQueue('admin_exports');
            }

            \DB::commit();
        } catch (\Exception $e) {
            \DB::rollBack();
            throw $e;
        }
    }

    public function h5Async($fileName)
    {
        $this->setFilePath('h5_exports/', $fileName)->setChannel('h5');

        if (null === $this->queue) {
            $this->onQueue('h5_exports');
        }
    }

    public function getFilePath()
    {
        return $this->filePath;
    }

    public function setFilePath($prefix = '', $path = '')
    {
        $this->filePath = $prefix . Carbon::now()->format('Y-m-d') . '/' . $path;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getTaskId()
    {
        return $this->taskId;
    }

    /**
     * @param $taskId
     * @return $this
     */
    public function setTaskId($taskId)
    {
        $this->taskId = $taskId;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getChannel()
    {
        return $this->channel;
    }

    public function setChannel($channel)
    {
        $this->channel = $channel;

        return $this;
    }

    /**
     * 去除生僻字
     * @param $string
     * @return false|string
     */
    public function removeUncommonWords($string)
    {
        return iconv('GBK', 'UTF-8', iconv('UTF-8', 'GBK//IGNORE', $string));
    }

    /**
     * 每次查询条数
     * @return int
     */
    public function chunkSize(): int
    {
        return 200;
    }


    /**
     * @return bool
     */
    public function isNeedSendMail(): bool
    {
        return $this->needSendMail;
    }

    /**
     * @param bool $needSendMail
     */
    public function setNeedSendMail(bool $needSendMail)
    {
        $this->needSendMail = $needSendMail;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getFileName()
    {
        return $this->fileName;
    }

    /**
     * @param mixed $fileName
     */
    public function setFileName($fileName)
    {
        $this->fileName = $fileName;

        return $this;
    }
}
